<script setup lang="ts">
import { computed, onBeforeMount, ref, type PropType } from 'vue'
import axios, { type AxiosResponse, type AxiosRequestHeaders } from 'axios'
import { v4 as uuidv4 } from 'uuid'
import { last } from 'lodash-es'
import { DeleteOutlined, LoadingOutlined } from '@ant-design/icons-vue'

export type CheckUpload = (file: File) => boolean | Promise<File>
type UploadStatus = 'ready' | 'loading' | 'success' | 'error'
type FileListType = 'picture' | 'text'
export interface UploadFile {
  uid: string
  size: number
  name: string
  status: UploadStatus
  raw: File
  resp?: any
  // 上传为图书时候的预览地址
  url?: string
}

const fileInputRef = ref<HTMLInputElement | null>(null)
const props = defineProps({
  // 上传地址
  action: {
    type: String,
    required: true
  },
  beforeUpload: {
    type: Function as PropType<CheckUpload>
  },
  // the name of uploading file
  name: {
    type: String,
    default: 'file'
  },
  // request header
  headers: {
    type: Object as PropType<AxiosRequestHeaders>,
    default: () => {
      return { 'Content-Type': 'multipart/form-data' }
    }
  },
  // 携带 cookie
  withCredentials: {
    type: Boolean,
    default: false
  },
  // input accept file type
  accept: {
    type: String,
    required: false
  },
  // whether to support drag
  drag: {
    type: Boolean,
    default: false
  },
  autoUpload: {
    type: Boolean,
    default: true
  },
  listType: {
    type: String as PropType<FileListType>,
    default: 'text'
  },
  showUploadList: {
    type: Boolean,
    default: true
  }
})
const emits = defineEmits(['on-success', 'on-error', 'on-progress', 'on-change'])

// 上传的文件列表
const fileList = ref<UploadFile[]>([])
const lastFileData = computed(() => {
  const lastFile = last(fileList.value)
  if (!lastFile) {
    return false
  }
  return {
    loaded: lastFile.status === 'success',
    data: lastFile.resp
  }
})
const isUploading = computed(() => {
  return fileList.value.some((item) => item.status === 'loading')
})
const isDragOver = ref(false)
// 拖拽中
const handleDrag = (e: DragEvent, over: boolean) => {
  e.preventDefault()
  isDragOver.value = over
}
// 拖拽结束，松手
const handleDrop = (e: DragEvent) => {
  e.preventDefault()
  isDragOver.value = false
  if (!e.dataTransfer?.files) {
    return
  }
  beforeUploadCheck(e.dataTransfer.files)
}

const postFile = async (readyFile: UploadFile) => {
  const formData = new FormData()
  formData.append(props.name, readyFile.raw)

  // 发送请求
  let res: AxiosResponse | null = null
  try {
    // 上传相同的文件，onchange事件不会被触发，需要上传完成后，重置value
    if (fileInputRef.value) {
      fileInputRef.value.value = ''
    }
    readyFile.status = 'loading'
    res = await axios.post(props.action, formData, {
      headers: props.headers,
      withCredentials: props.withCredentials
    })
  } catch (error) {
    readyFile.status = 'error'
    emits('on-error', error)
  }
  if (!res || !res.data) {
    emits('on-error', '服务端返回信息错误')
    readyFile.status = 'error'
    return
  }
  // console.log(res)
  readyFile.status = 'success'
  emits('on-success', { resp: res.data, file: readyFile, list: fileList })
  readyFile.resp = res.data
}
const addFileToList = async (uploadedFile: File) => {
  const fileObj = ref<UploadFile>({
    uid: uuidv4(),
    size: uploadedFile.size,
    name: uploadedFile.name,
    // 这个文件的状态
    status: 'ready',
    raw: uploadedFile
  })
  if (props.listType === 'picture') {
    try {
      fileObj.value.url = URL.createObjectURL(fileObj.value.raw)
    } catch (error) {
      console.error('upload File Error', error)
    }
  }
  fileList.value.push(fileObj.value)
  if (props.autoUpload) {
    await postFile(fileObj.value)
  }
}
// 手动上传
const uploadFiles = () => {
  fileList.value
    .filter((file) => file.status === 'ready')
    .forEach((readyFile) => postFile(readyFile))
}
const beforeUploadCheck = async (files: FileList) => {
  const uploadedFile = files[0]
  if (props.beforeUpload) {
    const result = props.beforeUpload(uploadedFile)
    if (result instanceof Promise) {
      const processFile = await result
      // 判断 processFile 是否为文件类型
      if (processFile instanceof File) {
        await addFileToList(processFile)
      } else {
        throw new Error('beforeUpload must return a boolean or Promise<File>')
      }
    } else if (result === true) {
      await addFileToList(uploadedFile)
    }
  } else {
    await addFileToList(uploadedFile)
  }
}

const handleFileChange = async (e: Event) => {
  const target = e.target as HTMLInputElement
  if (!target.files) {
    return
  }
  emits('on-change', e, fileList)
  await beforeUploadCheck(target.files)
}
const removeFile = async (uid: UploadFile['uid']) => {
  const index = fileList.value.findIndex((item) => item.uid === uid)
  if (index === -1) {
    return
  }
  const item = fileList.value[index]
  if (item.status === 'loading') {
    item.status = 'ready'
  }
  fileList.value.splice(index, 1)
}

const triggerUpload = () => {
  if (!fileInputRef.value) {
    return
  }
  fileInputRef.value.click()
}

let events: { [key: string]: (e: any) => void } = {
  click: triggerUpload
}

onBeforeMount(() => {
  if (props.drag) {
    events = {
      ...events,
      dragover: (e: DragEvent) => {
        handleDrag(e, true)
      },
      dragleave: (e: DragEvent) => {
        handleDrag(e, false)
      },
      drop: handleDrop
    }
  }
})
defineExpose({
  uploadFiles
})
</script>

<template>
  <div class="uploader">
    <div class="upload-area" :class="{ 'is-dragover': props.drag && isDragOver }" v-on="events">
      <slot v-if="isUploading" name="loading">
        <button disabled>正在上传</button>
      </slot>
      <slot
        name="uploaded"
        v-else-if="lastFileData && lastFileData.loaded"
        :uploadedData="lastFileData.data"
      >
        <button>点击上传</button>
      </slot>
      <slot v-else name="default">
        <button>点击上传</button>
      </slot>
    </div>
    <input
      type="file"
      style="display: none"
      :accept="props.accept"
      ref="fileInputRef"
      @change="handleFileChange"
    />
    <ul :class="`upload-list upload-list-${props.listType}`" v-if="props.showUploadList">
      <li :class="`uploaded-file upload-${item.status}`" v-for="item in fileList" :key="item.uid">
        <img
          v-if="props.listType === 'picture' && item.url"
          :src="item.url"
          :alt="item.name"
          srcset=""
        />
        <span class="filename">{{ item.name }}</span>
        <span
          class="delete-icon"
          @click="removeFile(item.uid)"
          :class="`delete-icon-${item.status}`"
        >
          <LoadingOutlined class="loading-icon" v-if="item.status === 'loading'" />
          <DeleteOutlined v-else />
        </span>
      </li>
    </ul>
  </div>
</template>

<style scoped lang="scss">
.uploader {
  margin-left: 8px;
  .upload-list {
    list-style-type: none;
    padding: 0;
    margin: 0;
    margin-left: 2px;
    .uploaded-file {
      display: flex;
      align-items: center;
      line-height: 28px;
      margin-bottom: 8px;
      &:hover {
        .delete-icon {
          display: flex;
        }
      }
      &.upload-loading {
        color: #ccc;
        &::before {
          background: #ccc;
        }
      }
      &.upload-success {
        color: green;
        &::before {
          background: green;
        }
      }
      &.upload-error {
        color: red;
        &::before {
          background: red;
        }
      }
      &::before {
        content: '';
        display: block;
        background: black;
        height: 20px;
        width: 3px;
        margin-right: 8px;
      }
      .filename {
        font-size: 22px;
      }
      .delete-icon {
        font-size: 18px;
        padding-left: 8px;
        cursor: pointer;
        color: initial;
        height: 28px;
        display: none;
        align-items: center;
        &.delete-icon-loading {
          display: flex;
          cursor: auto;
        }
      }
    }
  }
}
</style>
